package com.matrix.utils;
import java.util.ArrayList;
import java.util.regex.Pattern;
import com.matrix.math.*;

/**
 * Parser to convert a given String to matrix, vector, column vector, or a constant.
 * 
 * Expected Matrix input: [1, 2, 3, 4; 2, 3, 4, 5; 1, 2, 3, 4]
 * 						 OR
 * 						  [1 2 3 4 ; 2 3 4 5; 1 2 3 4]
 * 						 OR
 * 						  1 2 3 4;1 2 3 4; 1 2 3 4
 * 						 OR 
 * 					   	  [1#2#3#4;1#2#3%4;1#2#3#4]
 * 
 * Expected Column Vector input: [1, 2, 3, 4]';
 * 
 * Expected Coordinate Vector input: <1i, 2j, 3k>
 * 									OR
 * 									 <1i 2j  3k>
 * 
 * Note: All objects follow the format <*> = <input string> if they have names.
 * 
 * Checks:	1- Matrix needs to be in the right format
 *			2- Handles all issues of matrix input
 * @author Sultan
 * @version 1.00
 * @date Feb 10th, 2011
 * @see http://www.code.google.com/p/matrixcalculator
**/
public class Parser
{
	/**
	 * Instance Variables
	 */
	final static int HAS_ONE_ROW = 99;
	// the following tags are returned to tell what type of object was parsed in the call to 'parse'.
	final static int MATRIX = 1;
	final static int VECTOR = 2;
	final static int CONST = 3;
	final static int COLVECT = 4;
	
	
	private String token;
	private int numCol = 0, numRow = 0;
	private String separator = ";"; // users can choose this char in the settings
	private String[] array;
	
	// objects to be filled inside the parse function.
	private Matrix matrix;
	private Vector vector;
	private Constant constant;
	private ColumnVector colVect;
	
	/**
	 * Default Constructor
	 */
	public Parser()
	{
		this.setToken("");
	}
	
	/**
	 * Overridden Constructor
	 */
	public Parser(String what)
	{
		this.setToken(what);
	}
	
	/**
	 * Method to parse a string which may be a Matrix, Vector, Column Vector, or a Constant.
	 * 
	 * @return 1 for matrix
	 * 		   2 for Vector
	 * 		   3 for Constant
	 * 		   4 for Column vector
	 * @param none
	 * @throws Exception if the string to be parsed is empty
	 * 		   or if there is an error in the string.
	 */
	public int parse() throws Exception
	{
		String name = "";
		if(this.getToken().equalsIgnoreCase(""))
		{
			throw new Exception("You left the field empty.");
		}
		if(hasName())
		{
			name = this.parseName();
		}

		this.setToken(trim(getToken()));
		
		if(this.token.startsWith("<")) // vector parse
		{
			this.parseVector(name);
			return VECTOR;
		}
		else if(isConstant()) 			// constant parse
		{
			this.constant.setName(name);
			return CONST;
		}
		else if(getToken().endsWith("]'"))
		{
			this.parseColVector(name);
			return COLVECT;
		}
		else if(getToken().contains(";") || allNumerical(getToken()))	// add more checks // matrix parse
		{
			this.parseMatrix(name);
			return MATRIX;
		}
		else
		{
			throw new Exception("Input cannot be parsed");
		}
	}
	
	/**
	 * This method has a pattern that matches certain substrings and returns an array
	 * of strings. This method checks if those elements returned from the pattern are
	 * numerical.
	 * @param a String object 
	 * @return true if the returned array is numerical. false otherwise.
	 */
	public boolean allNumerical(String what)
	{
		Pattern p = Pattern.compile("[,[^-^0-9^.^e^E^-^+]]+"); // this pattern get doubles (negatives e values)
		String[] array = this.fixArray(p.split(this.getToken()));
		for(int i = 0; i < array.length; ++i)
		{
			if(!isDigit(array[i]))
			{
				return false;
			}
		}
		return true;
	}
	
	/**
	 * Method to check if 'token' is constant.
	 * @return true if token is constant, false otherwise.
	 */
	private boolean isConstant()
	{
		String s = this.trim(this.getToken());
		if(this.isDigit(s))
		{
			this.constant = new Constant(Double.parseDouble(s));
			return true;
		}
		return false;
	}
	
	/**
	 * Method to parse a name in the token string.
	 * @return void
	 */
	public String parseName()
	{
		int indexOfEqual = this.getToken().indexOf("=");
		String s = this.getToken().substring(0,indexOfEqual);
		this.setToken(this.getToken().substring(indexOfEqual+1));
		return trim(s);
	}
	
	/**
	 * Method to check if the string 'token' has a name simply by checking 
	 * if the string contains the character '='.
	 * @return true if '=' was found in the string, and false otherwise.
	 */
	public boolean hasName()
	{
		if(this.getToken().contains("="))
		{
			return true;
		}
		else
			return false;
	}
	
	/**
	 * Method to parse a string and store the values in a Vector object.
	 * @param a String object
	 * @return void
	 * @throws exception if the string is not in a vector format.
	 */
	public void parseVector(String name) throws Exception
	{
		if(!this.getToken().endsWith(">"))
		{
			throw new Exception("wrong Vector formatt");
		}
		else
		{
			//get rid of angle brackets
			setToken(getToken().substring(1));
			setToken(getToken().substring(0, getToken().length()-1));
			setToken(trim(getToken()));
			//get first component
			
			Pattern p = Pattern.compile("[,[^-^0-9^.^e^E^-^+]]+"); // this pattern get doubles (negatives e values)
			String[] array = this.fixArray(p.split(getToken()));
			
			if(array.length > 3)
			{
				throw new Exception("more than three componenets");
			}
			double[] what = new double[3];
			// check if the string contains any characters other than i, j, k

			Pattern p1 = Pattern.compile("[,[ijk][-+]?\\d*\\.?\\d+([eE][-+]?\\d+)? \\s \\[\\]]+");
			String[] array1 = this.fixArray(p1.split(getToken()));

			if(array1.length != 0) throw new Exception("Invalid characters in vector");
			
			int counter = 0;
			if(getToken().contains("i") || getToken().contains("I")) counter++;
			if(getToken().contains("j") || getToken().contains("J")) counter++;
			if(getToken().contains("k") || getToken().contains("K")) counter++;

			if(counter != array.length)
			{
				throw new Exception("You are missing a letter.");
			}
			
			if(getToken().contains("i"))
			{
				what[0] = Double.parseDouble(array[0]);
			}
			else what[0] = 0;
			
			if(getToken().contains("j"))
			{
				what[1] = Double.parseDouble(array[1]);
			}
			else what[1] = 0;
			
			if(getToken().contains("k"))
			{
				what[2] = Double.parseDouble(array[2]);
			}
			else what[2] = 0;
			
			this.vector = new Vector(name,what);
		}// end else
	}// end parseVector
	
	/**
	 * Method to parse string and store the values in a column vector object.
	 * @param a String object.
	 * @throws Exception if the string is not in a column vector format.
	 * @return void.
	 */
	public void parseColVector(String name) throws Exception
	{
		setToken(getToken().substring(1));
		setToken(getToken().substring(0, getToken().length()-2));
		this.setToken(trim(getToken()));
 		if(!this.checkFormat())
		{
			throw new Exception("Wrong Col Vector Format");
		}
 		if(!this.checkDelimiter())
 		{
			throw new Exception("Wrong Col Vector Delimiter Format");
 		}
		else
		{
			double[] a = new double[getNumCol()];
			for(int i = 0; i < getNumCol(); ++i)
			{
				a[i] = Double.parseDouble(this.array[i]);
			}
			this.colVect = new ColumnVector(name, a);
		}
	}

	/**
	 * This function parse the string stored in the parser object and store the values in a matrix
	 * object.
	 * @param a string object representing the name of the matrix.
	 * @return void
	 * @throws exception if the string is not in a matrix format.
	 */
	public void parseMatrix(String name) throws Exception
	{
		
		if(!this.checkFormat())
		{
			throw new Exception("Wrong Matrix Format");
		}
		if(!this.checkDelimiter())
		{
			throw new Exception("Wrong Matrix Delimiter Format");
		}
		else
		{
			int count = 0;
			this.matrix = new Matrix(this.getNumRow(),this.getNumCol());
			this.matrix.setName(name);
			for(int i = 0; i < this.getNumRow(); i++)
			{
				for(int j = 0; j < this.getNumCol(); ++j)
				{
					String a = this.array[count++];
					this.matrix.setValueAt(i, j, Double.parseDouble(a));
				}
			}
			//this.clear();
		}
	}

	/**
	 * This function checks the format of the string stored in the parser object.
	 * The pseduocode for this function is as follows:
	 * 1- validate that one of the following conditions is true:
	 * 			- Condition one: The string starts with '[' and ends with ']'
	 * 			- Condtion two: The string start with a number and ends with a number.
	 * 		if both condtions are false, return -1 which means the format is wrong.
	 * 2- remove all commas, and nonDigit characters from the string so that all remaining 
	 * 	  characters are the order of the matrix.
	 * 	  NOTE that the "fixArray" function is used here because the split 
	 * 		   function in the Pattern class returns an array whose first cell
	 * 		   is empty, so we use the function fixArray to delete the empty cell.
	 * 3- assign tche length of the array to the variable NbyM which is the total number
	 * 	  of elements in the matrix.
	 * 4- assign the number of columns by calling the function "getColDimension".
	 * 5- perform a check to see if the remainder of dividing NbyM by the number of columns
	 * 	  equals to zero. If remainder is zero that means there is no missing elements in the matrix
	 * 	  and the input is correct, else return -1 for input error.
	 * 6- set the number of rows to to the result of dividing NbyM by numOfColumns.
	 * 	  this variable will be used in other functions.
	 * 
	 * @return positive integer if format is good, else return -1
	 */
	public boolean checkFormat()
	{
		int NbyM = 0; // number of columns times number of rows
		String firstChar = this.getToken().substring(0,1);
		String lastChar = this.getToken().substring(this.getToken().length()-1);
		boolean condition = this.getToken().startsWith("[") && this.getToken().endsWith("]");
		boolean condition2 = (this.isDigit(firstChar)|| firstChar.equals("-")) && this.isDigit(lastChar);
		if(!(condition || condition2))
		{
			return false;
		}
		//Pattern p = Pattern.compile("[,[^-^0-9^.^e^E^[+-]]]+"); // All elements
		Pattern p = Pattern.compile("[,[^-^0-9^.^e^E^-^+]]+"); // this pattern get doubles (negatives e values)
		this.array = this.fixArray(p.split(this.getToken()));

		NbyM = this.array.length;
		int numOfCols = this.getColDimension(this.getToken());
		if(numOfCols == HAS_ONE_ROW)
		{
			this.setNumCol(this.array.length);
			this.setNumRow(1);
			return true;
		}
		this.setNumCol(this.getColDimension(this.getToken()));
		if(NbyM % this.getNumCol() != 0) return false;
		this.setNumRow(NbyM/this.getNumCol());
		return true;
	}
	
	/**t
	 * replace the first occurance of a string in a given string.
	 * @return
	 */
	public String replaceFirst(String parent, String a, String b)
	{
		// parent is rset
		int index = parent.indexOf(a);
		String first = "";
		String second = "";
		first = parent.substring(0, index);
		second = parent.substring(index + a.length());
		parent = first + b + second;
		return parent;
	}
	/**
	 * Method to convert a string to an array of strings where each cell is one character.
	 * white spaces are not returned by this function.
	 * @return array of strings
	 */
	public ArrayList<String> getChars(String what)
	{
		String a;
		ArrayList<String> set = new ArrayList<String>();
		for(int i = 0; i < what.length(); ++i)
		{
			a = what.substring(i, i+1);
			if(!a.equals(" "))
			{
				set.add(a);
			}
		}
		return set;
	}
	/**
	 * if the user chooses to separate the entries by non-digit character,
	 * then this method checks to see if those delimiters are inputed correctly
	 * @return
	 */
	public boolean checkDelimiter()
	{		
		String rset;
		Pattern p = Pattern.compile("[,[^-^0-9^.^e^E^-^+]]+"); // this pattern get doubles (negatives e values)
		String[] temp = this.fixArray(p.split(this.getToken()));
		rset = this.getToken();
		for(int i = 0; i < temp.length; ++i)
		{
			rset = this.replaceFirst(rset, temp[i], " ");
		}
		ArrayList<String> temp1 = this.getChars(rset);
		int numOfDelimiter = ((this.getNumCol()-1) * this.getNumRow())+ this.decrement(this.getNumRow());
		if(temp1.size() == this.decrement(this.getNumRow()))
		{
			return true;
		}
		if(temp1.size() == numOfDelimiter)
		{
			String head = "";
			String next = "";
			for(int i = 0; i < temp1.size(); ++i)
			{
				head = temp1.get(i);
				if(i+1 < temp1.size())
					next = temp1.get(i+1);
				else
					next = head;
				if(!next.equals(this.separator))
				{
					if(!head.equals(next))
					{
						return false;
					}
				}
				else
				{
					i++;
				}
			}
			return true;
		}
		return false;
	}
	
	/**
	 * Method to get the number of columns in the first row in a matrix string.
	 * @param a String object.
	 * @return number of columns.
	 */
	public int getColDimension(String what)
	{
	
		String temp = "";
		int indexOfSeparator = -1;
		for(int i = 0; i < what.length(); ++i)
		{
			temp = what.substring(i,i+1);
			if(temp.equalsIgnoreCase(this.separator))
			{
				indexOfSeparator = i;
				break;
			}
		}// end for
		if(indexOfSeparator != -1) // means it s a row vector
		{
			temp = what.substring(0, indexOfSeparator+1);
			Pattern p = Pattern.compile("[,[^0-9^.^E^e^-]]+");	   
			String[] array = this.fixArray(p.split(temp));
			return array.length;
		}
		return HAS_ONE_ROW;
	}	
	
	
	/**
	 * Helper Methods
	 */
	
	/***
	 * Method to check if the argumnet string is a number.
	 * @param a String object.
	 * @return true if parameter is digit, else returns false.
	 * @type boolean
	 */
	private boolean isDigit(String what)
	{
		try
		{
			Double.parseDouble(what);
			return true;
		}
		catch(Exception e)
		{
			return false;
		}
	}// end private boolean isDigit(String what)
	
	/**
	 * This method checks if the first cell in the passed array 
	 * is empty and if it is then it returns a new copy of the array
	 * without the empty cell.
	 * @param array of type String
	 * @return a copy array if the condition above is met, 
	 * 			otherwise it returns the same string.
	 */
	public String[] fixArray(String[] what)
	{
		if(what.length == 0)
		{
			return what;
		}
		String[] temp = new String[what.length-1];
		if(what[0].equalsIgnoreCase(""))
		{
			for(int i = 0; i < what.length-1; i++)
			{
				temp[i] = what[i+1];
			}
			return temp;
		}
		return what;
	}

	/**
	 * setters and getters
	 */
	public String getToken(){return token;}
	public void setToken(String token) {this.token = this.deleteWSatStart(token);}
	protected int getNumCol() {return numCol;}
	protected void setNumCol(int numCol) {this.numCol = numCol;}
	protected int getNumRow() {return numRow;}
	protected void setNumRow(int numRow){this.numRow = numRow;}
	public Matrix getMatrix(){return this.matrix;}
	public void setMatrix(Matrix what){this.matrix = what;}
	public Vector getVector(){return this.vector;}
	public void setVector(Vector what){this.vector = what;}
	public Constant getConstant(){return this.constant;}
	public void setConstant(Constant what){this.constant = what;}
	public ColumnVector getColVect(){return this.colVect;}
	public void setColVect(ColumnVector what){this.colVect = what;}
	public int decrement(int what){return what-1;}

	/**
	 * Method to delete the white spaces at the head of a string.
	 * @param a String object.
	 * @return the result string.
	 */
	public String deleteWSatStart(String what)
	{
		if(what.length() != 0)
		{
			String first = what.substring(0,1);
			while(first.equals(" "))
			{
				what = what.substring(1);
				first = what.substring(0,1);
			}
		}
		return what;
	}
	
	/**
	 * Method to delete the white spaces at the tail of a string.
	 * @param a String object.
	 * @return the result string.
	 */
	public String deleteWSatEnd(String what)
	{
		char last = what.charAt(what.length()-1);
		while(last == ' ')
		{
			what = what.substring(0,what.length()-1);
			last = what.charAt(what.length()-1);
		}
		return what;
	}
	/**
	 * Method to delete the white spaces at the head and tail of a string.
	 * @param a String object.
	 * @return the result string.
	 */
	public String trim(String what)
	{
		String s = "";
		s = deleteWSatStart(what);
		s = deleteWSatEnd(what);
		return s;
	}
	/**
	 * Method to clear the attributes in the parser class.
	 */
	public void clear()
	{
		this.setNumRow(0);
		this.setNumCol(0);
		this.setToken("");
		this.array = null;
		this.matrix = null;
	}
} //EOF